/*******************************************************************************
 * Copyright (c) 2012-2016 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.rest;

import com.google.gwt.core.client.Callback;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestBuilder.Method;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.user.client.Timer;

import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.api.promises.client.callback.CallbackPromiseHelper;
import org.eclipse.che.ide.commons.exception.JobNotFoundException;
import org.eclipse.che.ide.commons.exception.ServerException;

import static com.google.gwt.http.client.Response.SC_ACCEPTED;
import static com.google.gwt.http.client.Response.SC_NOT_FOUND;

/**
 * Wrapper under {@link RequestBuilder} to simplify the stuffs.
 *
 * @author Artem Zatsarynnyi
 */
public class AsyncRequest {
    protected RequestBuilder requestBuilder;
    protected int asyncTaskCheckingPeriodMillis = 5000;

    private AsyncRequestCallback<?>      callback;
    private boolean                      async;
    private Timer                        checkAsyncTaskStatusTimer;
    private AsyncRequestCallback<String> asyncRequestCallback;
    private String                       asyncTaskStatusURL;
    private AsyncRequestLoader           loader;
    private RequestStatusHandler         asyncTaskStatusHandler;

    /**
     * Create new {@link AsyncRequest} instance.
     *
     * @param method
     *         request method
     * @param url
     *         request URL
     * @param async
     *         if {@code true} - request will be send in asynchronous mode (as asynchronous EverRest task).<br>
     *         See <a href="https://github.com/codenvy/everrest/wiki/Asynchronous-Requests">
     *         EverRest Asynchronous requests</a> for details.
     */
    protected AsyncRequest(Method method, String url, boolean async) {
        if (async) {
            if (url.contains("?")) {
                url += "&async=true";
            } else {
                url += "?async=true";
            }
        }

        this.requestBuilder = new RequestBuilder(method, url);
        this.async = async;
        this.checkAsyncTaskStatusTimer = new CheckEverRestTaskStatusTimer();
        this.asyncRequestCallback = new EverRestAsyncRequestCallback();
    }

    public final AsyncRequest header(String header, String value) {
        requestBuilder.setHeader(header, value);
        return this;
    }

    public final AsyncRequest user(String user) {
        requestBuilder.setUser(user);
        return this;
    }

    public final AsyncRequest password(String password) {
        requestBuilder.setPassword(password);
        return this;
    }

    public final AsyncRequest data(String requestData) {
        requestBuilder.setRequestData(requestData);
        return this;
    }

    public final AsyncRequest loader(AsyncRequestLoader loader) {
        this.loader = loader;
        return this;
    }

    /**
     * Set period for checking asynchronous task status. (5000 ms by default).<br>
     * Makes sense for request sent in async mode only.
     *
     * @param period
     *         period of checking asynchronous task status (in milliseconds)
     * @return this {@link AsyncRequest}
     */
    public final AsyncRequest period(int period) {
        this.asyncTaskCheckingPeriodMillis = period;
        return this;
    }

    /**
     * Set handler of async task status. Makes sense for request sent in async mode only.
     *
     * @param handler
     *         handler to set
     * @return this {@link AsyncRequest}
     */
    public final AsyncRequest requestStatusHandler(RequestStatusHandler handler) {
        this.asyncTaskStatusHandler = handler;
        return this;
    }

    /**
     * Sends an HTTP request based on the current {@link AsyncRequest} configuration.
     *
     * @return promise that may be resolved with the {@link Void} or rejected in case request error
     */
    public final Promise<Void> send() {
        return CallbackPromiseHelper.createFromCallback(new CallbackPromiseHelper.Call<Void, Throwable>() {
            @Override
            public void makeCall(final Callback<Void, Throwable> callback) {
                send(new AsyncRequestCallback<Void>() {
                    @Override
                    protected void onSuccess(Void result) {
                        callback.onSuccess(null);
                    }

                    @Override
                    protected void onFailure(Throwable exception) {
                        callback.onFailure(exception);
                    }
                });
            }
        });
    }

    /**
     * Sends an HTTP request based on the current {@link AsyncRequest} configuration.
     *
     * @param unmarshaller
     *         unmarshaller that should be used to deserialize a response
     * @return promise that may be resolved with the deserialized response value or rejected in case request error
     */
    public final <R> Promise<R> send(final Unmarshallable<R> unmarshaller) {
        return CallbackPromiseHelper.createFromCallback(new CallbackPromiseHelper.Call<R, Throwable>() {
            @Override
            public void makeCall(final Callback<R, Throwable> callback) {
                send(new AsyncRequestCallback<R>(unmarshaller) {
                    @Override
                    protected void onSuccess(R result) {
                        callback.onSuccess(result);
                    }

                    @Override
                    protected void onFailure(Throwable exception) {
                        callback.onFailure(exception);
                    }
                });
            }
        });
    }

    /**
     * Sends an HTTP request based on the current {@link AsyncRequest} configuration.
     *
     * @param callback
     *         the response handler to be notified when the request fails or completes
     */
    public final void send(AsyncRequestCallback<?> callback) {
        this.callback = callback;
        try {
            if (async) {
                this.callback.setRequest(this);
                sendRequest(asyncRequestCallback);
            } else {
                sendRequest(callback);
            }
        } catch (RequestException e) {
            callback.onFailure(e);
        }
    }

    private void sendRequest(AsyncRequestCallback<?> callback) throws RequestException {
        callback.setLoader(loader);
        callback.setRequest(this);
        requestBuilder.setCallback(callback);

        if (loader != null) {
            loader.show();
        }

        requestBuilder.send();
    }

    /**
     * Returns the callback of current {@link AsyncRequest}, or null if no callback was set.
     *
     * @return the callback that to be notified when the request fails or completes
     */
    public AsyncRequestCallback<?> getCallback() {
        return callback;
    }

    public RequestBuilder getRequestBuilder() {
        return requestBuilder;
    }

    /** Timer that checks status of the EverRest asynchronous task caused by this request. */
    private class CheckEverRestTaskStatusTimer extends Timer {
        @Override
        public void run() {
            final RequestBuilder requestBuilder = new RequestBuilder(RequestBuilder.GET, asyncTaskStatusURL);
            requestBuilder.setCallback(new RequestCallback() {
                @Override
                public void onResponseReceived(Request request, Response response) {
                    if (SC_NOT_FOUND == response.getStatusCode()) {
                        callback.onError(request, new JobNotFoundException(response));
                        if (asyncTaskStatusHandler != null) {
                            asyncTaskStatusHandler.requestError(asyncTaskStatusURL, new JobNotFoundException(response));
                        }
                    } else if (response.getStatusCode() != SC_ACCEPTED) {
                        callback.onResponseReceived(request, response);
                        if (asyncTaskStatusHandler != null) {
                            // check is response successful, for correct handling failed responses
                            if (callback.isSuccessful(response)) {
                                asyncTaskStatusHandler.requestFinished(asyncTaskStatusURL);
                            } else {
                                asyncTaskStatusHandler.requestError(asyncTaskStatusURL, new ServerException(response));
                            }
                        }
                    } else {
                        if (asyncTaskStatusHandler != null) {
                            asyncTaskStatusHandler.requestInProgress(asyncTaskStatusURL);
                        }
                        CheckEverRestTaskStatusTimer.this.schedule(asyncTaskCheckingPeriodMillis);
                    }
                }

                @Override
                public void onError(Request request, Throwable exception) {
                    if (asyncTaskStatusHandler != null) {
                        asyncTaskStatusHandler.requestError(asyncTaskStatusURL, exception);
                    }

                    callback.onError(request, exception);
                }
            });

            try {
                requestBuilder.send();
            } catch (RequestException e) {
                if (asyncTaskStatusHandler != null) {
                    asyncTaskStatusHandler.requestError(asyncTaskStatusURL, e);
                }
                callback.onFailure(e);
            }
        }
    }

    /** Callback that will be called on response to a request that was sent in EverRest async mode. */
    private class EverRestAsyncRequestCallback extends AsyncRequestCallback<String> {

        EverRestAsyncRequestCallback() {
            super(new LocationUnmarshaller());
            setSuccessCodes(new int[]{SC_ACCEPTED});
        }

        @Override
        protected void onSuccess(String result) {
            asyncTaskStatusURL = result;
            if (asyncTaskStatusHandler != null) {
                asyncTaskStatusHandler.requestInProgress(asyncTaskStatusURL);
            }
            checkAsyncTaskStatusTimer.schedule(asyncTaskCheckingPeriodMillis);
        }

        @Override
        protected void onFailure(Throwable exception) {
            if (asyncTaskStatusHandler != null) {
                asyncTaskStatusHandler.requestError(asyncTaskStatusURL, exception);
            }
            callback.onError(null, exception);
        }
    }
}
